using System;
using System.Collections;
using System.IO;

using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Cms;
using Org.BouncyCastle.Asn1.Cms.Ecc;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.Utilities;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;

namespace Org.BouncyCastle.Cms
{
	/**
	* the RecipientInfo class for a recipient who has been sent a message
	* encrypted using key agreement.
	*/
	public class KeyAgreeRecipientInformation
		: RecipientInformation
	{
		private KeyAgreeRecipientInfo info;
		private Asn1OctetString       encryptedKey;

		internal static void ReadRecipientInfo(IList infos, KeyAgreeRecipientInfo info,
			CmsSecureReadable secureReadable)
		{
			try
			{
				foreach (Asn1Encodable rek in info.RecipientEncryptedKeys)
				{
					RecipientEncryptedKey id = RecipientEncryptedKey.GetInstance(rek.ToAsn1Object());

					RecipientID rid = new RecipientID();

					Asn1.Cms.KeyAgreeRecipientIdentifier karid = id.Identifier;

					Asn1.Cms.IssuerAndSerialNumber iAndSN = karid.IssuerAndSerialNumber;
					if (iAndSN != null)
					{
						rid.Issuer = iAndSN.Name;
						rid.SerialNumber = iAndSN.SerialNumber.Value;
					}
					else
					{
						Asn1.Cms.RecipientKeyIdentifier rKeyID = karid.RKeyID;

						// Note: 'date' and 'other' fields of RecipientKeyIdentifier appear to be only informational 

						rid.SubjectKeyIdentifier = rKeyID.SubjectKeyIdentifier.GetOctets();
					}

					infos.Add(new KeyAgreeRecipientInformation(info, rid, id.EncryptedKey,
						secureReadable));
				}
			}
			catch (IOException e)
			{
				throw new ArgumentException("invalid rid in KeyAgreeRecipientInformation", e);
			}
		}

		internal KeyAgreeRecipientInformation(
			KeyAgreeRecipientInfo	info,
			RecipientID				rid,
			Asn1OctetString			encryptedKey,
			CmsSecureReadable		secureReadable)
			: base(info.KeyEncryptionAlgorithm, secureReadable)
		{
			this.info = info;
			this.rid = rid;
			this.encryptedKey = encryptedKey;
		}

		private AsymmetricKeyParameter GetSenderPublicKey(
			AsymmetricKeyParameter		receiverPrivateKey,
			OriginatorIdentifierOrKey	originator)
		{
			OriginatorPublicKey opk = originator.OriginatorPublicKey;
			if (opk != null)
			{
				return GetPublicKeyFromOriginatorPublicKey(receiverPrivateKey, opk);
			}
			
			OriginatorID origID = new OriginatorID();
			
			Asn1.Cms.IssuerAndSerialNumber iAndSN = originator.IssuerAndSerialNumber;
			if (iAndSN != null)
			{
				origID.Issuer = iAndSN.Name;
				origID.SerialNumber = iAndSN.SerialNumber.Value;
			}
			else
			{
				SubjectKeyIdentifier ski = originator.SubjectKeyIdentifier;

				origID.SubjectKeyIdentifier = ski.GetKeyIdentifier();
			}

			return GetPublicKeyFromOriginatorID(origID);
		}

		private AsymmetricKeyParameter GetPublicKeyFromOriginatorPublicKey(
			AsymmetricKeyParameter	receiverPrivateKey,
			OriginatorPublicKey		originatorPublicKey)
		{
			PrivateKeyInfo privInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(receiverPrivateKey);
			SubjectPublicKeyInfo pubInfo = new SubjectPublicKeyInfo(
				privInfo.AlgorithmID,
				originatorPublicKey.PublicKey.GetBytes());
			return PublicKeyFactory.CreateKey(pubInfo);
		}

		private AsymmetricKeyParameter GetPublicKeyFromOriginatorID(
			OriginatorID origID)
		{
			// TODO Support all alternatives for OriginatorIdentifierOrKey
			// see RFC 3852 6.2.2
			throw new CmsException("No support for 'originator' as IssuerAndSerialNumber or SubjectKeyIdentifier");
		}

		private KeyParameter CalculateAgreedWrapKey(
			string					wrapAlg,
			AsymmetricKeyParameter	senderPublicKey,
			AsymmetricKeyParameter	receiverPrivateKey)
		{
			DerObjectIdentifier agreeAlgID = keyEncAlg.ObjectID;

			ICipherParameters senderPublicParams = senderPublicKey;
			ICipherParameters receiverPrivateParams = receiverPrivateKey;

			if (agreeAlgID.Id.Equals(CmsEnvelopedGenerator.ECMqvSha1Kdf))
			{
				byte[] ukmEncoding = info.UserKeyingMaterial.GetOctets();
				MQVuserKeyingMaterial ukm = MQVuserKeyingMaterial.GetInstance(
					Asn1Object.FromByteArray(ukmEncoding));

				AsymmetricKeyParameter ephemeralKey = GetPublicKeyFromOriginatorPublicKey(
					receiverPrivateKey, ukm.EphemeralPublicKey);

				senderPublicParams = new MqvPublicParameters(
					(ECPublicKeyParameters)senderPublicParams,
					(ECPublicKeyParameters)ephemeralKey);
				receiverPrivateParams = new MqvPrivateParameters(
					(ECPrivateKeyParameters)receiverPrivateParams,
					(ECPrivateKeyParameters)receiverPrivateParams);
			}

			IBasicAgreement agreement = AgreementUtilities.GetBasicAgreementWithKdf(
				agreeAlgID, wrapAlg);
			agreement.Init(receiverPrivateParams);
			BigInteger agreedValue = agreement.CalculateAgreement(senderPublicParams);

			int wrapKeySize = GeneratorUtilities.GetDefaultKeySize(wrapAlg) / 8;
			byte[] wrapKeyBytes = X9IntegerConverter.IntegerToBytes(agreedValue, wrapKeySize);
			return ParameterUtilities.CreateKeyParameter(wrapAlg, wrapKeyBytes);
		}

		private KeyParameter UnwrapSessionKey(
			string			wrapAlg,
			KeyParameter	agreedKey)
		{
			byte[] encKeyOctets = encryptedKey.GetOctets();

			IWrapper keyCipher = WrapperUtilities.GetWrapper(wrapAlg);
			keyCipher.Init(false, agreedKey);
			byte[] sKeyBytes = keyCipher.Unwrap(encKeyOctets, 0, encKeyOctets.Length);
			return ParameterUtilities.CreateKeyParameter(GetContentAlgorithmName(), sKeyBytes);
		}

		internal KeyParameter GetSessionKey(
			AsymmetricKeyParameter receiverPrivateKey)
		{
			try
			{
				string wrapAlg = DerObjectIdentifier.GetInstance(
					Asn1Sequence.GetInstance(keyEncAlg.Parameters)[0]).Id;

				AsymmetricKeyParameter senderPublicKey = GetSenderPublicKey(
					receiverPrivateKey, info.Originator);

				KeyParameter agreedWrapKey = CalculateAgreedWrapKey(wrapAlg,
					senderPublicKey, receiverPrivateKey);

				return UnwrapSessionKey(wrapAlg, agreedWrapKey);
			}
			catch (SecurityUtilityException e)
			{
				throw new CmsException("couldn't create cipher.", e);
			}
			catch (InvalidKeyException e)
			{
				throw new CmsException("key invalid in message.", e);
			}
			catch (Exception e)
			{
				throw new CmsException("originator key invalid.", e);
			}
		}

		/**
		* decrypt the content and return an input stream.
		*/
		public override CmsTypedStream GetContentStream(
			ICipherParameters key)
		{
			if (!(key is AsymmetricKeyParameter))
				throw new ArgumentException("KeyAgreement requires asymmetric key", "key");

			AsymmetricKeyParameter receiverPrivateKey = (AsymmetricKeyParameter) key;

			if (!receiverPrivateKey.IsPrivate)
				throw new ArgumentException("Expected private key", "key");

			KeyParameter sKey = GetSessionKey(receiverPrivateKey);

			return GetContentFromSessionKey(sKey);
		}
	}
}
